package edu.njit.hz278.core.web;

import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.util.HashMap;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.codehaus.jackson.JsonParseException;
import org.codehaus.jackson.map.JsonMappingException;


/**
 * Global Servlet. All requests will be dispatched to their corresponding
 * handlers by this default servlet. The handler is organized according to the
 * served URL: /application/module/method. We refer to `application' as the
 * sub-package, `module' as the class, and `method' as the java method which
 * actually processes user requests.
 * 
 * @author Rivers Zhang
 * @version 1.0.0
 */

@WebServlet("/*")
public class Dispatcher extends HttpServlet {

	/**
	 * Methods for sending HTTP error code
	 */

	private static void send304(HttpServletResponse resp) throws IOException {
		resp.setStatus(304);
	}

	private static void send400(HttpServletResponse resp) throws IOException {
		resp.sendError(400);
	}

	private static void send404(HttpServletResponse resp) throws IOException {
		resp.sendError(404);
	}

	private static void send501(HttpServletResponse resp) throws IOException {
		resp.sendError(501);
	}

	/**
	 * Return the associated handler.
	 * 
	 * @param req
	 * @return
	 */
	private Handler getHandler(String method, String path) {
		return handlers.get(method + ":" + path);
	}

	private void maybeSetLastModified(HttpServletResponse resp,
			long lastModified) {
		if (resp.containsHeader(HEADER_LASTMOD))
			return;
		if (lastModified >= 0)
			resp.setDateHeader(HEADER_LASTMOD, lastModified);
	}

	@Override
	protected void service(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {

		String method = req.getMethod();

		if (method.equals(METHOD_OPTIONS)) {
			doOptions(req, resp);
			return;
		}

		if (method.equals(METHOD_TRACE)) {
			doTrace(req, resp);
			return;
		}

		if (method.equals(METHOD_HEAD)) {
			doHead(req, resp);
			return;
		}

		if (method.equals(METHOD_GET)) {
			long lastModified = getLastModified(req);
			if (lastModified != -1) {
				long ifModifiedSince = req.getDateHeader(HEADER_IFMODSINCE);
				if (ifModifiedSince >= lastModified)
					maybeSetLastModified(resp, lastModified);
				else
					send304(resp);
			}
		}

		if (!method.equals(METHOD_GET) && !method.equals(METHOD_POST)
				&& !method.equals(METHOD_PUT) && !method.equals(METHOD_DELETE)) {
			send501(resp);
			return;
		}

		/**
		 * okay! we've already handled all bloody exceptional situations.
		 */

		Handler h = getHandler(req.getMethod(), req.getPathInfo());

		if (h == null)
			send404(resp);
		else
			try {
				h.invoke(req, resp);
			} catch (IllegalAccessException | IllegalArgumentException
					| InvocationTargetException | JsonParseException
					| JsonMappingException | InstantiationException e) {
				log.warn("Method invocation error.", e);
				send400(resp);
			}

	}

	static void registerHandler(String key, Handler value) {
		handlers.put(key, value);
	}

	/**
	 * `handlers' holds the mapping from request uri to its corresponding
	 * handler.
	 */
	private static final HashMap<String, Handler> handlers = new HashMap<String, Handler>();

	private static final String METHOD_DELETE = "DELETE";
	private static final String METHOD_HEAD = "HEAD";
	private static final String METHOD_GET = "GET";
	private static final String METHOD_OPTIONS = "OPTIONS";
	private static final String METHOD_POST = "POST";
	private static final String METHOD_PUT = "PUT";
	private static final String METHOD_TRACE = "TRACE";

	private static final String HEADER_IFMODSINCE = "If-Modified-Since";
	private static final String HEADER_LASTMOD = "Last-Modified";

	private static final Logger log = LogManager.getLogger(Dispatcher.class);

	private static final long serialVersionUID = -3895211475588075571L;

}
